/*
 * Copyright 2009-2010 Belmont Software Services
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.belmont.backup.server;

import java.net.*;
import com.belmont.backup.*;
import java.io.*;
import java.util.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.mortbay.jetty.*;
import org.mortbay.jetty.servlet.*;
import org.mortbay.jetty.security.*;
import org.mortbay.jetty.nio.*;
import org.json.*;

public class BackupService implements IBackupConstants {
    static final String DEFAULT_USER = "admin";
    static final String DEFAULT_PASSWORD = "admin";
    int port;
    File root;
    File backupRoot;
    Server server;
    boolean enabled;
    long startedOn = System.currentTimeMillis();
    Config config;
    File configFile;
    File logDir;
    FileBackupServer fserver;
    BackupUserRealm realm;
    static InetAddress localhost;
    static BackupService instance;

    public BackupService(int port) {
        this(new File("."), null);
    }

    public BackupService(File root, Config c) {
        this.root = root;
        this.logDir = new File(root, "logs");
        Utils.openLog(logDir, "backupservice.log");
        configFile = new File(root, SERVER_CONFIG_FILE);
        init(c);
    }

    void init(Config c) {
        Utils.log(LOG_INFO, "Initializing server");
        if (c == null) {
            this.config = readConfig();
        } else {
            this.config = c;
        }
        this.port = config.getPort();
        instance = this;
        if (config.getBackupRoot() == null) {
            backupRoot = root;
        } else {
            backupRoot = new File(config.getBackupRoot());
        }
        fserver = new FileBackupServer(backupRoot);
        fserver.setConfig(config);
        startedOn = System.currentTimeMillis();
        Utils.log(LOG_INFO, "Initialized server on port "+port+" backup root "+backupRoot.getAbsolutePath());
    }

    public IBackupServer getServer() {
        return fserver;
    }

    public Config readConfig() {
        Utils.log(LOG_INFO, "Read config from "+configFile.getAbsolutePath());
        if (configFile.exists()) {
            try {
                DataInputStream in = new DataInputStream(new FileInputStream(configFile));
                try {
                    return new Config(new JSONObject(in.readUTF()));
                } finally {
                    in.close();
                }
            } catch (IOException ex) {
                Utils.log(LOG_ERROR, "Error in readConfig", ex);
            } catch (JSONException ex) {
                Utils.log(LOG_ERROR, "Error in readConfig", ex);
            }

            return new Config();
        } else {
            Utils.log(LOG_INFO, "Config file doesn't exist, returning null config "+configFile.getAbsolutePath());
            return new Config();
        }
    }

    public Config getConfig() {
        return config;
    }

    public void saveConfig() throws IOException {
        DataOutputStream out = new DataOutputStream(new FileOutputStream(configFile));
        try {
            out.writeUTF(config.getJSON().toString());
        } catch (JSONException ex) {
            Utils.log(LOG_ERROR, "Error saving configuration", ex);
            throw new IOException("JSON format error in config "+ex.toString());
        } finally {
            out.close();
        }
    }

    public static BackupService getInstance() {
        return instance;
    }

    public File getLogDirectory() {
        return logDir;
    }

    public String getBackupDirectory() {
        String r = config.getBackupRoot();

        if (r == null) {
            return root.getAbsolutePath();
        } else {
            return r;
        }
    }

    public Vector<String[]> getClients() throws SQLException, IOException {
        return fserver.getClients();
    }

    public String getServiceHost() {
        if (localhost == null) {
            try {
                localhost = InetAddress.getLocalHost();
            } catch (UnknownHostException e) {
                Utils.log(LOG_ERROR, "Error in getServiceHost", e);
            }
        }
        if (localhost == null) {
            Utils.log(LOG_ERROR, "Error resolving localhost");
            return "!unknown";
        } else {
            return localhost.getHostName();
        }
    }

    public String getServiceIP() {
        if (localhost == null) {
            try {
                localhost = InetAddress.getLocalHost();
            } catch (UnknownHostException e) {
                Utils.log(LOG_ERROR, "Error in getServiceIP", e);
            }
        }
        if (localhost == null) {
            Utils.log(LOG_ERROR, "Error resolving IP for localhost");
            return "!unknown";
        } else {
            return localhost.getHostAddress();
        }
    }

    class RestartThread implements Runnable {
        BackupService svc;
        int seconds;

        RestartThread(BackupService svc, int seconds) {
            this.svc = svc;
            this.seconds = seconds;
        }

        public void run() {
            try {
                if (seconds > 0) {
                    Thread.sleep(seconds * 1000);
                }
                Utils.log(LOG_INFO, "Restarting service");
                svc.stop();
                svc.init(null);
                svc.start();
                Utils.log(LOG_INFO, "Service restarted");
            } catch (InterruptedException ex) {
                Utils.log(LOG_ERROR, "Interruped in restart", ex);
            } catch (Exception ex) {
                Utils.log(LOG_ERROR, "Error restarting service", ex);
            }
        }
    }

    public String restart(int seconds) {
        new Thread(new RestartThread(this, seconds)).start();
        return "Service restarting on "+new java.util.Date(System.currentTimeMillis()+(seconds *1000)).toString();
    }

    public String getServicePort() {
        return Integer.toString(port);
    }

    public String getStartedOn() {
        return new java.util.Date(startedOn).toString();
    }

    public boolean isEnabled() {
        return config.isServiceEnabled();
    }

    public void stop() {
        if (fserver != null) {
            fserver.stop();
        }
        try {
            server.stop();
        } catch (Exception ex) {
            Utils.log(LOG_ERROR, "Error stopping web service", ex);
        }
    }

/*
  import org.mortbay.jetty.security.*;

  Server server = new Server();

  Connector connector = new SelectChannelConnector();
  connector.setPort(8080);
  server.setConnectors(new Connector[]{connector});

  Constraint constraint = new Constraint();
  constraint.setName(Constraint.__BASIC_AUTH);;
  constraint.setRoles(new String[]{"user","admin","moderator"});
  constraint.setAuthenticate(true);

  ConstraintMapping cm = new ConstraintMapping();
  cm.setConstraint(constraint);
  cm.setPathSpec("/*");

  SecurityHandler sh = new SecurityHandler();
  sh.setUserRealm(new HashUserRealm("MyRealm",System.getProperty("jetty.home")+"/etc/realm.properties"));
  sh.setConstraintMappings(new ConstraintMapping[]{cm});

  WebAppContext webappcontext = new WebAppContext();
  webappcontext.setContextPath("/mywebapp");
  webappcontext.setWar("./path/to/my/war/orExplodedwar");
  webappcontext.addHandler(sh);

  HandlerCollection handlers= new HandlerCollection();
  handlers.setHandlers(new Handler[]{webappcontext, new DefaultHandler()});

  server.setHandler(handlers);
  server.start();
  server.join();
*/

    static class BackupUserRealm extends HashUserRealm {
        HashMap<String, String> users = new HashMap<String, String>();
        File configFile;
        String adminUser;

        public BackupUserRealm(File configFile) {
            this.configFile = configFile;
        }

        public BackupUserRealm(String name, String configPath) throws IOException {
            super(name, configPath);
            configFile = new File(configPath);
            readConfigFile();
        }

        void readConfigFile() throws IOException {
            DataInputStream din = new DataInputStream(new FileInputStream(configFile));
            try {
                String line;
                while ((line = din.readLine()) != null) {
                    line = line.trim();
                    if (line.length() > 0) {
                        int idx = line.indexOf(':');
                        if (idx == -1) {
                            Utils.log(LOG_ERROR, "Improper syntax in password file");
                            continue;
                        }
                        adminUser = line.substring(0, idx);
                        String pass = null;
                        line = line.substring(idx+1);
                        idx = line.indexOf(',');
                        if (idx == -1) {
                            pass = line.trim();
                        } else {
                            pass = line.substring(0, idx).trim();
                        }
                        if (pass.startsWith("OBF:")) {
                            pass = Password.deobfuscate(pass);
                        }
                        users.put(adminUser, pass);
                    }
                }
            } finally {
                din.close();
            }
        }

        void saveConfigFile() throws IOException {
            FileOutputStream out = new FileOutputStream(configFile);
            try {
                Iterator<String> ki = users.keySet().iterator();
                while (ki.hasNext()) {
                    String u = ki.next();
                    String p = users.get(u);

                    String l = u+": "+Password.obfuscate(p)+",admin\n";
                    out.write(l.getBytes());
                }
            } finally {
                out.close();
            }
        }

        String getAdminUser() {
            return adminUser;
        }

        String getAdminPassword() {
            return users.get(adminUser);
        }

        void setAdminUser(String user, String pass) {
            users.clear();
            users.put(user, pass);
            adminUser = user;
            try {
                saveConfigFile();
                Utils.log(LOG_INFO, "Admin user changed");
            } catch (IOException ex) {
                Utils.log(LOG_ERROR, "Error saving password file", ex);
            }
        }

        HashMap getUsers() {
            return _users;
        }

        HashMap getRoles() {
            return _roles;
        }
    }

    public String[] getAdminUser() {
        String ups[] = new String[2];
        ups[0] = realm.getAdminUser();
        ups[1] = realm.getAdminPassword();
        return ups;
    }

    public void setAdminUser(String ups[]) {
        if (ups != null && ups.length == 2) {
            realm.setAdminUser(ups[0], ups[1]);
        }
    }

    public void start() throws Exception {
        try {
            server = new Server();
            Connector connector = new SelectChannelConnector();
            connector.setPort(port);
            server.setConnectors(new Connector[]{connector});
            connector.setServer(server);

            Constraint constraint = new Constraint();
            constraint.setName(Constraint.__DIGEST_AUTH);
            constraint.setRoles(new String[]{"admin"});
            constraint.setAuthenticate(true);

            ConstraintMapping cm = new ConstraintMapping();
            cm.setConstraint(constraint);
            cm.setPathSpec("/ui/*");

            SecurityHandler sh = new SecurityHandler();
            File pf = new File(root+"/realm.properties");
            if (! pf.exists()) {
                BackupUserRealm r = new BackupUserRealm(pf);
                r.setAdminUser(DEFAULT_USER, DEFAULT_PASSWORD);
            }

            sh.setUserRealm(realm = new BackupUserRealm("Backup Server Console",pf.getAbsolutePath()));
            sh.setConstraintMappings(new ConstraintMapping[]{cm});

            Context context = new Context(server, "/",Context.SESSIONS);
            context.setClassLoader(getClass().getClassLoader());
            context.setResourceBase(new File(root, "webapps").getAbsolutePath());
            Utils.log(LOG_INFO, "Resource base "+new File(root,"webapps"));
            context.addServlet(new ServletHolder(new BackupUIServlet()), "/ui/admin/*");
            context.addServlet(new ServletHolder(new BackupProtocolServlet()), "/backup/*");
            context.addServlet(new ServletHolder(new BackupAdminServlet()), "/");
            context.addHandler(sh);
            server.start();
            enabled = true;
        } catch (Exception ex) {
            Utils.log(LOG_ERROR, "Error starting service", ex);
            throw ex;
        }
    }

    public static void main(String args[]) throws Exception {
        BackupService service;

        System.out.println("BackupService.main "+args.length);

        if (args.length > 0 ) {
            service = new BackupService(new File(args[0]), null);
        } else {
            service = new BackupService(DEFAULT_PORT);
        }

        service.start();
        Thread.currentThread().join();
    }
}